/*
 * PROJECT:     ReactOS Storage Stack / SCSIPORT storage port library
 * LICENSE:     GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
 * PURPOSE:     Logical Unit (PDO) functions
 * COPYRIGHT:   Eric Kohl (eric.kohl@reactos.org)
 *              Aleksey Bragin (aleksey@reactos.org)
 *              2020 Victor Perevertkin (victor.perevertkin@reactos.org)
 */

#include "scsiport.h"
#include "scsitypes.h"

#define NDEBUG
#include <debug.h>


PDEVICE_OBJECT
PdoCreateLunDevice(
    _In_ PSCSI_PORT_DEVICE_EXTENSION DeviceExtension)
{
    PSCSI_PORT_LUN_EXTENSION LunExtension;
    PDEVICE_OBJECT LunPDO;

    ULONG LunExtensionSize = DeviceExtension->LunExtensionSize + sizeof(SCSI_PORT_LUN_EXTENSION);

    NTSTATUS Status = IoCreateDevice(DeviceExtension->Common.DeviceObject->DriverObject,
                                     LunExtensionSize,
                                     NULL,
                                     FILE_DEVICE_DISK,
                                     FILE_AUTOGENERATED_DEVICE_NAME | FILE_DEVICE_SECURE_OPEN,
                                     FALSE,
                                     &LunPDO);

    if (!NT_SUCCESS(Status))
    {
        DPRINT1("Failed to create a Lun PDO, status: %x\n", Status);
        return NULL;
    }

    LunExtension = LunPDO->DeviceExtension;

    /* Zero everything */
    RtlZeroMemory(LunExtension, LunExtensionSize);

    LunExtension->Common.IsFDO = FALSE;
    LunExtension->Common.DeviceObject = LunPDO;
    LunExtension->Common.LowerDevice = DeviceExtension->Common.DeviceObject;

    /* Initialize a list of requests */
    InitializeListHead(&LunExtension->SrbInfo.Requests);

    /* Initialize timeout counter */
    LunExtension->RequestTimeout = -1;

    /* Set maximum queue size */
    LunExtension->MaxQueueCount = 256;

    /* Initialize request queue */
    KeInitializeDeviceQueue(&LunExtension->DeviceQueue);

    LunPDO->Flags |= DO_DIRECT_IO;
    LunPDO->Flags &= ~DO_DEVICE_INITIALIZING;

    return LunPDO;
}

PSCSI_PORT_LUN_EXTENSION
GetLunByPath(
    _In_ PSCSI_PORT_DEVICE_EXTENSION DeviceExtension,
    _In_ UCHAR PathId,
    _In_ UCHAR TargetId,
    _In_ UCHAR Lun)
{
    if (PathId >= DeviceExtension->NumberOfBuses)
    {
        DPRINT1("Invalid PathId: %u\n", PathId);
        return NULL;
    }

    PSCSI_BUS_INFO bus = &DeviceExtension->Buses[PathId];

    for (PLIST_ENTRY lunEntry = bus->LunsListHead.Flink;
         lunEntry != &bus->LunsListHead;
         lunEntry = lunEntry->Flink)
    {
        PSCSI_PORT_LUN_EXTENSION lunExt =
            CONTAINING_RECORD(lunEntry, SCSI_PORT_LUN_EXTENSION, LunEntry);

        if (lunExt->PathId == PathId &&
            lunExt->TargetId == TargetId &&
            lunExt->Lun == Lun)
        {
            return lunExt;
        }
    }

    DPRINT("SCSI LUN (%u, %u, %u) was not found\n", PathId, TargetId, Lun);
    return NULL;
}

PSCSI_REQUEST_BLOCK_INFO
SpiGetSrbData(
    _In_ PSCSI_PORT_DEVICE_EXTENSION DeviceExtension,
    _In_ PSCSI_PORT_LUN_EXTENSION LunExtension,
    _In_ UCHAR QueueTag)
{
    if (QueueTag == SP_UNTAGGED)
    {
        /* Return the pointer to SrbInfo */
        return &LunExtension->SrbInfo;
    }
    else
    {
        /* Make sure the tag is valid, if it is - return the data */
        if (QueueTag > DeviceExtension->SrbDataCount || QueueTag < 1)
            return NULL;
        else
            return &DeviceExtension->SrbInfo[QueueTag -1];
    }
}

static
ULONG
CopyField(
    IN PUCHAR Name,
    IN PCHAR Buffer,
    IN ULONG MaxLength,
    IN CHAR DefaultCharacter,
    IN BOOLEAN Trim)
{
    ULONG Index;

    for (Index = 0; Index < MaxLength; Index++)
    {
        if (Name[Index] <= ' ' || Name[Index] >= 0x7F /* last printable ascii character */ ||  Name[Index] == ',')
        {
            // convert to underscore
            Buffer[Index] = DefaultCharacter;
        }
        else
        {
            // just copy character
            Buffer[Index] = Name[Index];
        }
    }

    /* Trim trailing default characters */
    if (Trim)
    {
        Index = MaxLength - 1;
        for (;;)
        {
            if (Buffer[Index] != DefaultCharacter)
            {
                Index++;
                break;
            }

            Index--;
        }
    }

    return Index;
}

static
NTSTATUS
PdoHandleQueryDeviceText(
    _In_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp)
{
    PSCSI_PORT_LUN_EXTENSION DeviceExtension = DeviceObject->DeviceExtension;
    PIO_STACK_LOCATION IoStack;
    UINT32 Offset = 0;
    PINQUIRYDATA InquiryData;
    CHAR LocalBuffer[64];
    ANSI_STRING AnsiString;
    UNICODE_STRING DeviceDescription;

    DPRINT("PdoHandleQueryDeviceText\n");

    IoStack = IoGetCurrentIrpStackLocation(Irp);

    InquiryData = &DeviceExtension->InquiryData;

    switch (IoStack->Parameters.QueryDeviceText.DeviceTextType)
    {
        case DeviceTextDescription:
        {
            DPRINT("DeviceTextDescription\n");

            Offset += CopyField(InquiryData->VendorId,
                                &LocalBuffer[Offset],
                                sizeof(InquiryData->VendorId),
                                ' ',
                                TRUE);
            LocalBuffer[Offset++] = ' ';
            Offset += CopyField(InquiryData->ProductId,
                                &LocalBuffer[Offset],
                                sizeof(InquiryData->ProductId),
                                ' ',
                                TRUE);
            Offset += sprintf(&LocalBuffer[Offset],
                              " SCSI %s Device",
                              GetDeviceType(InquiryData));
            LocalBuffer[Offset++] = '\0';

            RtlInitAnsiString(&AnsiString, (PCSZ)&LocalBuffer);

            DeviceDescription.Length = 0;
            DeviceDescription.MaximumLength = (USHORT)(Offset * sizeof(WCHAR));
            DeviceDescription.Buffer = ExAllocatePoolWithTag(PagedPool,
                                                             DeviceDescription.MaximumLength,
                                                             TAG_SCSIPORT);
            if (!DeviceDescription.Buffer)
            {
                Irp->IoStatus.Information = 0;
                return STATUS_INSUFFICIENT_RESOURCES;
            }

            RtlAnsiStringToUnicodeString(&DeviceDescription, &AnsiString, FALSE);

            Irp->IoStatus.Information = (ULONG_PTR)DeviceDescription.Buffer;
            return STATUS_SUCCESS;
        }

        case DeviceTextLocationInformation:
        {
            DPRINT("DeviceTextLocationInformation\n");

            sprintf(LocalBuffer, "Bus Number %d, Target ID %d, LUN %d",
                    DeviceExtension->PathId, DeviceExtension->TargetId, DeviceExtension->Lun);

            RtlInitAnsiString(&AnsiString, (PCSZ)&LocalBuffer);

            DeviceDescription.Length = 0;
            DeviceDescription.MaximumLength = (USHORT)((strlen(LocalBuffer) + 1) * sizeof(WCHAR));
            DeviceDescription.Buffer = ExAllocatePoolWithTag(PagedPool,
                                                             DeviceDescription.MaximumLength,
                                                             TAG_SCSIPORT);
            if (!DeviceDescription.Buffer)
            {
                Irp->IoStatus.Information = 0;
                return STATUS_INSUFFICIENT_RESOURCES;
            }

            RtlAnsiStringToUnicodeString(&DeviceDescription, &AnsiString, FALSE);

            Irp->IoStatus.Information = (ULONG_PTR)DeviceDescription.Buffer;
            return STATUS_SUCCESS;
        }

        default:
        {
            Irp->IoStatus.Information = 0;
            return Irp->IoStatus.Status;
        }
    }
}

static
NTSTATUS
PdoHandleQueryDeviceId(
    _In_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp)
{
    PSCSI_PORT_LUN_EXTENSION DeviceExtension;
    NTSTATUS Status;
    CHAR Buffer[100] = {0};
    LPCSTR DeviceType;
    ULONG Offset = 0;
    PINQUIRYDATA InquiryData;
    ANSI_STRING AnsiString;
    UNICODE_STRING DeviceId;

    DeviceExtension = DeviceObject->DeviceExtension;
    InquiryData = &DeviceExtension->InquiryData;

    DeviceType = GetDeviceType(InquiryData);

    // lets create device string
    Offset = sprintf(&Buffer[Offset], "SCSI\\");
    Offset += sprintf(&Buffer[Offset], DeviceType);
    Offset += sprintf(&Buffer[Offset], "&Ven_");
    Offset += CopyField(InquiryData->VendorId, &Buffer[Offset], 8, '_', TRUE);
    Offset += sprintf(&Buffer[Offset], "&Prod_");
    Offset += CopyField(InquiryData->ProductId, &Buffer[Offset], 16, '_', TRUE);
    Offset += sprintf(&Buffer[Offset], "&Rev_");
    Offset += CopyField(InquiryData->ProductRevisionLevel, &Buffer[Offset], 4, '_', TRUE);
    Buffer[Offset] = '\0';

    RtlInitAnsiString(&AnsiString, (PCSZ)Buffer);

    // allocate DeviceId string
    Status = RtlAnsiStringToUnicodeString(&DeviceId, &AnsiString, TRUE);

    if (NT_SUCCESS(Status))
    {
        Irp->IoStatus.Information = (ULONG_PTR)DeviceId.Buffer;
    }

    DPRINT("DeviceId %wZ Status %x\n", &DeviceId, Status);

    return Status;
}

static
VOID
ConvertToUnicodeString(
    IN CHAR * Buffer,
    IN ULONG ResultBufferLength,
    IN ULONG ResultBufferOffset,
    OUT LPWSTR ResultBuffer,
    OUT PULONG NewResultBufferOffset)
{
    UNICODE_STRING DeviceString;
    ANSI_STRING AnsiString;
    NTSTATUS Status;

    ASSERT(ResultBufferLength);
    ASSERT(ResultBufferLength > ResultBufferOffset);

    DPRINT("ResultBufferOffset %lu ResultBufferLength %lu Buffer %s Length %lu\n",
        ResultBufferOffset, ResultBufferLength, Buffer, strlen(Buffer));

    // construct destination string
    DeviceString.Buffer = &ResultBuffer[ResultBufferOffset];
    DeviceString.Length = 0;
    DeviceString.MaximumLength = (ResultBufferLength - ResultBufferOffset) * sizeof(WCHAR);

    // initialize source string
    RtlInitAnsiString(&AnsiString, Buffer);

    Status = RtlAnsiStringToUnicodeString(&DeviceString, &AnsiString, FALSE);
    ASSERT(Status == STATUS_SUCCESS);

    // subtract consumed bytes
    ResultBufferLength -= (DeviceString.Length + sizeof(WCHAR)) / sizeof(WCHAR);
    ResultBufferOffset += (DeviceString.Length + sizeof(WCHAR)) / sizeof(WCHAR);

    *NewResultBufferOffset = ResultBufferOffset;
}

static
NTSTATUS
PdoHandleQueryHardwareId(
    _In_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp)
{
    PSCSI_PORT_LUN_EXTENSION PDODeviceExtension = DeviceObject->DeviceExtension;
    LPCSTR GenericType, DeviceType;
    LPWSTR Buffer;
    CHAR Id1[50], Id2[50], Id3[50], Id4[50], Id5[50], Id6[50];
    ULONG Id1Length, Id2Length, Id3Length, Id4Length, Id5Length, Id6Length;
    ULONG Offset, TotalLength, Length;
    PINQUIRYDATA InquiryData;

    InquiryData = &PDODeviceExtension->InquiryData;

    DeviceType = GetDeviceType(InquiryData);
    GenericType = GetGenericType(InquiryData);

    ASSERT(GenericType);

    // generate id 1
    // SCSI\SCSIType_VendorId(8)_ProductId(16)_Revision(4)
    RtlZeroMemory(Id1, sizeof(Id1));
    Offset = 0;
    Offset = sprintf(&Id1[Offset], "SCSI\\");
    Offset += sprintf(&Id1[Offset], DeviceType);
    Offset += CopyField(InquiryData->VendorId, &Id1[Offset], 8, '_', FALSE);
    Offset += CopyField(InquiryData->ProductId, &Id1[Offset], 16, '_', FALSE);
    Offset += CopyField(InquiryData->ProductRevisionLevel, &Id1[Offset], 4, '_', FALSE);
    Id1Length = strlen(Id1) + 1;
    DPRINT("PdoHandleQueryHardwareId HardwareId1 %s\n", Id1);

    // generate id 2
    // SCSI\SCSIType_VendorId(8)_ProductId(16)
    RtlZeroMemory(Id2, sizeof(Id2));
    Offset = 0;
    Offset = sprintf(&Id2[Offset], "SCSI\\");
    Offset += sprintf(&Id2[Offset], DeviceType);
    Offset += CopyField(InquiryData->VendorId, &Id2[Offset], 8, '_', FALSE);
    Offset += CopyField(InquiryData->ProductId, &Id2[Offset], 16, '_', FALSE);
    Id2Length = strlen(Id2) + 1;
    DPRINT("PdoHandleQueryHardwareId HardwareId2 %s\n", Id2);

    // generate id 3
    // SCSI\SCSIType_VendorId(8)
    RtlZeroMemory(Id3, sizeof(Id3));
    Offset = 0;
    Offset = sprintf(&Id3[Offset], "SCSI\\");
    Offset += sprintf(&Id3[Offset], DeviceType);
    Offset += CopyField(InquiryData->VendorId, &Id3[Offset], 8, '_', FALSE);
    Id3Length = strlen(Id3) + 1;
    DPRINT("PdoHandleQueryHardwareId HardwareId3 %s\n", Id3);

    // generate id 4
    // SCSI\VendorId(8)_ProductId(16)_Revision(1)
    RtlZeroMemory(Id4, sizeof(Id4));
    Offset = 0;
    Offset = sprintf(&Id4[Offset], "SCSI\\");
    Offset += CopyField(InquiryData->VendorId, &Id4[Offset], 8, '_', FALSE);
    Offset += CopyField(InquiryData->ProductId, &Id4[Offset], 16, '_', FALSE);
    Offset += CopyField(InquiryData->ProductRevisionLevel, &Id4[Offset], 1, '_', FALSE);
    Id4Length = strlen(Id4) + 1;
    DPRINT("PdoHandleQueryHardwareId HardwareId4 %s\n", Id4);

    // generate id 5
    // VendorId(8)_ProductId(16)_Revision(1)
    RtlZeroMemory(Id5, sizeof(Id5));
    Offset = 0;
    Offset = CopyField(InquiryData->VendorId, &Id5[Offset], 8, '_', FALSE);
    Offset += CopyField(InquiryData->ProductId, &Id5[Offset], 16, '_', FALSE);
    Offset += CopyField(InquiryData->ProductRevisionLevel, &Id5[Offset], 1, '_', FALSE);
    Id5Length = strlen(Id5) + 1;
    DPRINT("PdoHandleQueryHardwareId HardwareId5 %s\n", Id5);

    // generate id 6
    // SCSIType
    RtlZeroMemory(Id6, sizeof(Id6));
    Offset = 0;
    Offset = sprintf(&Id6[Offset], GenericType);
    Id6Length = strlen(Id6) + 1;
    DPRINT("PdoHandleQueryHardwareId HardwareId6 %s\n", Id6);

    TotalLength = Id1Length + Id2Length + Id3Length + Id4Length + Id5Length + Id6Length + 1;

    Buffer = ExAllocatePoolWithTag(PagedPool, TotalLength * sizeof(WCHAR), TAG_SCSIPORT);
    if (!Buffer)
    {
        Irp->IoStatus.Information = 0;
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    // reset offset
    Offset = 0;
    Length = TotalLength;

    ConvertToUnicodeString(Id1, Length, Offset, Buffer, &Offset);
    ConvertToUnicodeString(Id2, Length, Offset, Buffer, &Offset);
    ConvertToUnicodeString(Id3, Length, Offset, Buffer, &Offset);
    ConvertToUnicodeString(Id4, Length, Offset, Buffer, &Offset);
    ConvertToUnicodeString(Id5, Length, Offset, Buffer, &Offset);
    ConvertToUnicodeString(Id6, Length, Offset, Buffer, &Offset);

    Buffer[Offset] = UNICODE_NULL;

    ASSERT(Offset + 1 == Length);

    Irp->IoStatus.Information = (ULONG_PTR)Buffer;
    return STATUS_SUCCESS;
}

static
NTSTATUS
PdoHandleQueryCompatibleId(
    _In_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp)
{
    PSCSI_PORT_LUN_EXTENSION PDODeviceExtension = DeviceObject->DeviceExtension;
    CHAR Buffer[100] = {0};
    ULONG Length, Offset;
    LPWSTR InstanceId;
    LPCSTR DeviceType;

    DeviceType = GetDeviceType(&PDODeviceExtension->InquiryData);

    // format instance id
    Length = sprintf(Buffer, "SCSI\\%s", DeviceType) + 1;
    Length += sprintf(&Buffer[Length], "SCSI\\%s", "RAW") + 2;

    InstanceId = ExAllocatePoolWithTag(PagedPool, Length * sizeof(WCHAR), TAG_SCSIPORT);
    if (!InstanceId)
    {
        Irp->IoStatus.Information = 0;
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    ConvertToUnicodeString(Buffer, Length, 0, InstanceId, &Offset);
    ConvertToUnicodeString(&Buffer[Offset], Length, Offset, InstanceId, &Offset);

    InstanceId[Offset] = UNICODE_NULL;

    DPRINT("PdoHandleQueryCompatibleId %S\n", InstanceId);

    Irp->IoStatus.Information = (ULONG_PTR)InstanceId;
    return STATUS_SUCCESS;
}

static
NTSTATUS
PdoHandleQueryInstanceId(
    _In_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp)
{
    PSCSI_PORT_LUN_EXTENSION lunExt = DeviceObject->DeviceExtension;
    WCHAR Buffer[26];
    ULONG Length;
    LPWSTR InstanceId;

    // use instance count and LUN
    swprintf(Buffer, L"%x%x%x", lunExt->PathId, lunExt->TargetId, lunExt->Lun);

    Length = wcslen(Buffer) + 1;

    InstanceId = ExAllocatePoolWithTag(PagedPool, Length * sizeof(WCHAR), TAG_SCSIPORT);
    if (!InstanceId)
    {
        Irp->IoStatus.Information = 0;
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    wcscpy(InstanceId, Buffer);

    DPRINT("PdoHandleQueryInstanceId %S\n", InstanceId);

    Irp->IoStatus.Information = (ULONG_PTR)InstanceId;
    return STATUS_SUCCESS;
}

static
NTSTATUS
PdoHandleDeviceRelations(
    _In_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp)
{
    PDEVICE_RELATIONS deviceRelations;
    PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);

    // check if relation type is BusRelations
    if (ioStack->Parameters.QueryDeviceRelations.Type != TargetDeviceRelation)
    {
        // PDO handles only target device relation
        return Irp->IoStatus.Status;
    }

    deviceRelations = ExAllocatePoolWithTag(PagedPool, sizeof(DEVICE_RELATIONS), TAG_SCSIPORT);
    if (!deviceRelations)
    {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    // initialize device relations
    deviceRelations->Count = 1;
    deviceRelations->Objects[0] = DeviceObject;
    ObReferenceObject(DeviceObject);

    Irp->IoStatus.Information = (ULONG_PTR)deviceRelations;
    return STATUS_SUCCESS;
}

NTSTATUS
PdoDispatchPnp(
    _In_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp)
{
    PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);
    PSCSI_PORT_LUN_EXTENSION lunExt = DeviceObject->DeviceExtension;
    NTSTATUS status;

    DPRINT("PDO PnP request %s\n", GetIRPMinorFunctionString(ioStack->MinorFunction));

    ASSERT(!lunExt->Common.IsFDO);

    switch (ioStack->MinorFunction)
    {
        case IRP_MN_START_DEVICE:
        {
            RegistryInitLunKey(lunExt);
            status = STATUS_SUCCESS;
            break;
        }
        case IRP_MN_REMOVE_DEVICE:
        case IRP_MN_QUERY_CAPABILITIES:
        case IRP_MN_QUERY_REMOVE_DEVICE:
        case IRP_MN_QUERY_STOP_DEVICE:
        case IRP_MN_SURPRISE_REMOVAL:
        {
            status = STATUS_SUCCESS;
            break;
        }
        case IRP_MN_QUERY_DEVICE_RELATIONS:
        {
            status = PdoHandleDeviceRelations(DeviceObject, Irp);
            break;
        }
        case IRP_MN_QUERY_DEVICE_TEXT:
        {
            status = PdoHandleQueryDeviceText(DeviceObject, Irp);
            break;
        }
        case IRP_MN_QUERY_ID:
        {
            DPRINT("IRP_MN_QUERY_ID IdType %s\n",
                DbgGetDeviceIDString(ioStack->Parameters.QueryId.IdType));

           if (ioStack->Parameters.QueryId.IdType == BusQueryDeviceID)
           {
               status = PdoHandleQueryDeviceId(DeviceObject, Irp);
               break;
           }
           else if (ioStack->Parameters.QueryId.IdType == BusQueryHardwareIDs)
           {
               status = PdoHandleQueryHardwareId(DeviceObject, Irp);
               break;
           }
           else if (ioStack->Parameters.QueryId.IdType == BusQueryInstanceID)
           {
               status = PdoHandleQueryInstanceId(DeviceObject, Irp);
               break;
           }
           else if (ioStack->Parameters.QueryId.IdType == BusQueryCompatibleIDs)
           {
               status = PdoHandleQueryCompatibleId(DeviceObject, Irp);
               break;
           }

           // fallthrough
        }
        default:
        {
            // do nothing
            status = Irp->IoStatus.Status;
        }
    }

    if (status != STATUS_PENDING)
    {
        Irp->IoStatus.Status = status;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }

    return status;
}
